import netCDF4
import copy
import re
import numpy
from .space import Space, SpaceList, Variable

# Global variable   Description
# ----------------- ------------------------------------------------------
# netcdf            netCDF4.Dataset instance
#
# nc                Dictionary relating netCDF variable names and
#                   netCDF4.Variable instances
#
# dim_to_ncdim      Dictionary relating space dimension names and
#                   netCDF dimension names
#
# ncdim_size        Dictionary relating netCDF dimension names and
#                   their sizes
#
# seen              Dictionary relating elements of the space (such as
#                   a coordinate or a transform) to their netCDF
#                   variable names and their netCDF dimensions
#
# ncvar_names       List of all netCDF dimension and netCDF variable
#                   names
#
# global_attributes Set of global or non-standard CF space attributes
#                   which have identical values across all input
#                   spaces.
#
# dimN              Integer
# auxN              Integer
# cmN               Integer
# dataN             Integer
# gmN               Integer
# bndN              Integer
# bnddimN           Integer
# strlenN           Integer
# ----------------- ------------------------------------------------------

# Initialize global variables
netcdf            = None
nc                = {}
dim_to_ncdim      = {}
ncdim_size        = {}
seen              = {} 
ncvar_names       = []
global_attributes = set(())
dimN              = 1
auxN              = 1
cmN               = 1
dataN             = 1
gmN               = 1
bndN              = 1
bnddimN           = 1
strlenN           = 1

def write(spaces, file, format='NETCDF3_CLASSIC', 
          data=False, verbose=False, 
          exact=False, **kwargs):
    '''

    Write a space, or sequence of spaces, to a netCDF(CF) file.

    NetCDF dimension and variable names will be taken from the
    attributes `ncvar` and `nc_dimensions` if present, otherwise they
    are inferred from standard_name attributes or set to
    defaults. NetCDF names may be automatically given a numerical
    suffix to avoid duplication.

    Output netCDF file global attributes are those which occur in the
    set of CF global attributes and non-standard data variable
    attributes and which have equal values across all input spaces.

    It is possible to write only a subset of the input spaces by
    providing phenomena conditions with `**kwargs` parameters.

    It does not matter if an input space's data has not yet been read
    into memory. The write operation will automatically read the data
    array from disk if required, but the default is for it not to be
    saved in the input space if it had to be read during the write
    operation. However, if the `data` argument equals True then any
    input spaces which didn't contain their full data arrays in memory
    will be changed in place so that they do. For example, for a
    space, s:

    >>> s.type
        <type 'netCDF4.Variable'>
    >>> write(s, 'filename')
    >>> s.type
        <type 'netCDF4.Variable'>
    >>> write(s, 'filename', data=True)
    >>> s.type
        <type 'numpy.ndarray'>

    Identical grid components (as determined by their numerically
    tolerant `equals` methods) are only written to the file once,
    apart from when they need to fulfil both dimension coordinate and
    auxiliary coordinate roles for different data variables.

    If a coordinate with a `transform` attribute is fully specified by
    another coordinate's transformation then it will be not written to
    the file.

    Arguments
    ---------
    spaces : Space or sequence of spaces
        The space or spaces to write to the file. If `**kwargs` are
        set for selection by phenomena then some spaces may not be
        written.
    file : str
        The netCDF file to write spaces to.

    Parameters
    ----------
    data : bool, optional
        If True then ensure that spaces which have been written to
        disk retain their data in-place as numpy arrays.
    exact : bool, optional
        If True then interpret the keywords from **kwargs as exact
        phenomenon names. Refer to `cf.Space.extract` for details.
    format : str, optional
        The format of the output netCDF file. Valid formats are those
        supported by the netCDF4 module: 'NETCDF3_CLASSIC',
        'NETCDF3_64BIT', 'NETCDF4_CLASSIC' and 'NETCDF4'. The default
        is 'NETCDF3_CLASSIC'. Certain write operations may be
        considerably faster using 'NETCDF4_CLASSIC'.
    verbose : bool, optional
        If True then print information to stdout.
    **kwargs
        Other keyword arguments defining space selection based on
        phenomena criteria. Only selected spaces will be written to
        disk. If none are set then all spaces are selected. Keywords
        and their arguments are set as for `cf.Space.extract`. This
        behaviour is modified by the `exact` keyword.
 
    Examples
    --------
    >>> s
    [<CF Space: air_pressure(30, 24)>,
     <CF Space: u_compnt_of_wind(19, 29, 24)>,
     <CF Space: v_compnt_of_wind(19, 29, 24)>,
     <CF Space: potential_temperature(19, 30, 24)>]
    >>> write(s, 'file')
    >>> write(s, 'file', standard_name = 'air_pressure')

    See `cf.Space.extract` for more examples of space selection by
    phenomena criteria'

    '''
    # See notes in this module for the definition of these global
    # variables
    global netcdf
    global nc
    global dim_to_ncdim
    global ncdim_size 
    global seen  
    global ncvar_names
    global dimN    
    global auxN    
    global cmN     
    global dataN   
    global gmN     
    global bndN    
    global bnddimN 
    global strlenN 
 
    def _grid_ncdimensions(s, key):
        
        # Global dictionary
        global dim_to_ncdim
        
        if (len(s.grid.dimensions[key]) == 1 and 
            s.grid.dimension_sizes[s.grid.dimensions[key][0]] <= 1):
            return ()

        ncdimensions = ()
        for dim in s.grid.dimensions[key]:
            ncdimensions += (dim_to_ncdim[dim],)

        return ncdimensions
    #--- End: def _grid_ncdimensions
    
    def _grid_ncvar(s, key, default, counter):

        try:
            ncvar = s.grid[key].ncvar
        except AttributeError:
            ncvar = s.grid[key].name(default=default)            
        return _check_name(ncvar, counter)        
    #--- End: def _grid_ncvar
    
    def _create_dimension(ncdim, s, dim):
        '''Create a netCDF dimension, but only if its size is greater
        than 1. An error will be raised if the dimension name has
        already been created. Returns None.'''
        
        # Global dictionaries
        global dim_to_ncdim, ncdim_size
        
        ncdim_size[ncdim] = s.grid.dimension_sizes[dim]
        
        if ncdim_size[ncdim] > 1:
            # Create a new dimension
            netcdf.createDimension(ncdim, ncdim_size[ncdim])
            dim_to_ncdim[dim] = ncdim
    #--- End: def _create_dimension

    def _create_dimension_coordinate(s, key):
        '''

    Create a dimension coordinate netCDF variable, creating a new
    bounds netCDF dimension if required, and create its bounds or
    climatology netCDF variable if it has one. Returns
    seen[id(s.grid[key])]['ncvar']).

    '''       
        # Global dictionaries
        global nc, dim_to_ncdim, seen
        # Global counters
        global dimN

        create = False
        if not _seen(s.grid[key]):            
            create = True
        elif seen[id(s.grid[key])]['ncdims'] != ():
            if seen[id(s.grid[key])]['ncvar'] != seen[id(s.grid[key])]['ncdims'][0]:
                # Already seen this coordinate, but it had a different
                # netCDF dimension name
                create = True

        if create:
            ncvar, dimN = _grid_ncvar(s, key, 'dim', dimN)
            
            # Create a new dimension, if it has size > 1
            _create_dimension(ncvar, s, key)
            
            ncdimensions = _grid_ncdimensions(s, key)
            
            # Create a new dimension coordinate variable
            _createVariable(ncvar, ncdimensions, s.grid[key], data=data)
            
            # If this dimension coordinate has bounds then add
            # the bounds attribute and create the bounds
            # netCDF variable
            for bounds_type in ('bounds', 'climatology'):
                ncbnd = _create_bounds(s, key, ncdimensions, bounds_type)
                if ncbnd is not None:
                    nc[ncvar].setncattr(bounds_type, ncbnd)
#                    nc[ncvar].bounds = ncbnd

        ncvar = seen[id(s.grid[key])]['ncvar']
        if ncdim_size[ncvar] > 1:
            dim_to_ncdim[key] = ncvar

        return ncvar
    #--- End: def _create_dimension_coordinate

    def _create_auxiliary_coordinate(s, key, coordinates):
        '''

    Create an auxiliary coordinate netCDF variable and create its
    bounds or climatology netCDF variable if it has one. Return the
    coordinates list updated with the ncvar for the auxiliary
    coordinate.
    
    '''
        # Global dictionaries
        global nc, seen
        # Global counters
        global auxN

        ncdimensions = _grid_ncdimensions(s, key)
        if not _seen(s.grid[key], ncdimensions):
            
            ncvar, auxN = _grid_ncvar(s, key, 'aux', auxN)
            
            # Create a new auxiliary coordinate variable
            _createVariable(ncvar, ncdimensions, s.grid[key],
                            data=data)
            
            # If this auxiliary coordinate has bounds then add the
            # bounds attribute and create the bounds netCDF variable
            for bounds_type in ('bounds', 'climatology'):
                ncbnd = _create_bounds(s, key, ncdimensions, bounds_type)
                if ncbnd is not None:
                    nc[ncvar].setncattr(bounds_type, ncbnd)
#                    nc[ncvar].bounds = ncbnd
              
        coordinates.append(seen[id(s.grid[key])]['ncvar'])

        return coordinates
    #--- End: def _create_auxiliary_coordinate

    def _create_bounds(s, key, coord_ncdimensions, bounds_type):
        '''

    Create a coordinate's bounds or climatology netCDF variable,
    creating a new bounds netCDF dimension if required. Return the
    bounds or climatology variable's netCDF variable name.

    '''
        # Global netCDF4.Dataset
        global netcdf
        # Global dictionaries
        global nc, seen, ncdim_size
        # Global counters
        global bndN, bnddimN
        
        try:
            bounds = getattr(s.grid[key], bounds_type)
            bounds._data
        except AttributeError:
            return None

        size = bounds.shape[-1]
        ncdim, bnddimN = _check_name('bounds', bnddimN, size)
        
        ncdimensions = coord_ncdimensions +(ncdim,)        
        if _seen(bounds, ncdimensions):
            return seen[id(bounds)]['ncvar']    
        
        try:
            ncdim_size[ncdim]
        except KeyError:
            ncdim_size[ncdim] = size
            netcdf.createDimension(ncdim, ncdim_size[ncdim])
        
        try:
            ncvar = bounds.ncvar
        except AttributeError:
            ncvar = ncvar+'_bounds'
            
        ncvar, bndN = _check_name(ncvar, bndN)

        # Create a bounds variable with no attributes
        _createVariable(ncvar, ncdimensions, bounds,  
                        omit=bounds.ncatts(), data=data)

        return ncvar
    #--- End: def _create_bounds
    
    def _seen(variable, ncdims=None):
        '''

    Private function.

    Return True if a variable identical to the input 'variable' (via
    variable.equals) is already in the 'seen' dictionary (and has
    therefore already been written to the output file), otherwise
    return False. When True is returned, the 'seen' dictionary is
    updated for the input 'variable'.

    If 'ncdims' is set then a further condition for returning True is
    that 'ncdims' is equal to netCDF dimensions (names and order) of
    the identical variable in the 'seen' dictionary.

    '''
        # Global dictionary
        global seen
        for ident in seen:
            if ncdims is not None and ncdims != seen[ident]['ncdims']:
                continue
            if variable.equals(seen[ident]['variable']):
                seen[id(variable)] = {'variable': variable,
                                      'ncvar'   : seen[ident]['ncvar'],
                                      'ncdims'  : seen[ident]['ncdims'],
                                      }
                return True
        return False
    #--- End: def _seen

    def _datatype(variable):
        '''

    Private function.

    Return the netCDF4.createVariable datatype corresponding to the
    datatype of the array stored (possibly virtually) in the input
    variable's 'data' attribute.

    Numpy string data types will return 'S1' regardless of the numpy
    string length. This means that the required conversion of
    multi-character datatype numpy arrays into single-character
    datatype numpy arrays (with an extra trailing dimension) is
    expected to be done elsewhere (currently in def _createVariable).

    If the input variable has no data attribute then 'S1' is returned.

    '''
        try:
            variable.dtype
        except AttributeError:
            return 'S1'

        if variable.dtype == 'float32': return 'f4'
        if variable.dtype == 'float64': return 'f8'
        if variable.dtype == 'float'  : return 'f8'
        if variable.dtype == 'int32'  : return 'i4'
        if variable.dtype == 'int64'  : return 'i8'
        if variable.dtype == 'int8'   : return 'i1'
        if variable.dtype == 'int16'  : return 'i2'
        if variable.dtype is None     : return 'S1'        
        if variable.dtype.char == 'S' : return 'S1'            

        return None
    #--- End: def _datatype

    def _createVariable(ncvar, ncdimensions, input, omit=set(()), 
                        data=False):
        '''

    Create a netCDF variable from 'input' with name 'ncvar' and
    dimensions 'ncdimensions'. The netCDF variable's attributes are
    given by input.properties(), less any given by the 'omit'
    argument. If a new string-length netCDF dimension is required then
    it will also be created. The 'seen' dictionary is updated for
    'input'.

    '''
        # Global netCDF4.Dataset
        global netcdf
        # Global dictionaries
        global seen, nc, ncdim_size
        # Global counters
        global strlenN

        # Set the netCDF4.createVariable datatype
        datatype = _datatype(input)

        dimensions = ncdimensions

        data_out = None
        if data:
            try:
                data_out = input.varray
            except AttributeError:
                pass            
        else:
            try:
                if not isinstance(input._data, numpy.ndarray):
                    pointer = input._data
                    data_out = input.varray
                    input._data = pointer
                else:
                    data_out = input.varray                    
            except AttributeError:
                pass
            

        # Convert a multi-character datatype numpy array into
        # single-character datatype numpy array with an extra trailing
        # dimension
        strlen = False
        if datatype == 'S1' and data_out is not None:
           strlen = int(re.findall('(\d+$)', str(input.dtype))[0])
           if strlen > 1:
               ncdim, strlenN = _check_name('strlen', strlenN, strlen)
               try:
                   ncdim_size[ncdim]
               except KeyError:
                   ncdim_size[ncdim] = strlen
               netcdf.createDimension(ncdim, ncdim_size[ncdim])
               dimensions = ncdimensions + (ncdim,)

               data_out = data_out.flatten()
               temp = numpy.empty((data_out.size, ncdim_size[ncdim]), '|S1')
               for i in xrange(data_out.size):
                   temp[i,:] = list(data_out[i].ljust(strlen, ' '))
               data_out = temp.reshape(input.shape + (ncdim_size[ncdim],))
               del temp

        # Create a new netCDF variable
        nc[ncvar] = netcdf.createVariable(ncvar, datatype, dimensions)

        # Add properties
	for attr in input.ncatts() - omit:
	    nc[ncvar].setncattr(attr, getattr(input, attr))

        # Add data
        if data_out is not None:
            nc[ncvar][...] = data_out

        del data_out

        # Update the 'seen' dictionary
        seen[id(input)] = {'variable': input, 'ncvar': ncvar, 'ncdims': dimensions}
    #--- End: def _createVariable

    def _create_grid_mapping(ncvar, input):
        '''
    
    Create a netCDF grid_mapping variable from `input` with name
    `ncvar`. The netCDF variable's attributes are given by key/value
    pairs of input.iteritems(). The 'seen' dictionary is updated for
    'input'.

    '''
        # Global netCDF4.Dataset
        global netcdf
        # Global dictionaries
        global seen, nc

        # Create a new netCDF variable
        nc[ncvar] = netcdf.createVariable(ncvar, 'S1', ())

        # Add attributes from key/value pairs
	for attr, value in input.iteritems():
	    nc[ncvar].setncattr(attr, value)

        # Update the 'seen' dictionary
        seen[id(input)] = {'variable': input, 'ncvar': ncvar, 'ncdims': ()}
    #--- End: def _create_grid_mapping

    def _check_name(base, counter, dimsize=None):

        # Global list
        global ncvar_names
        # Global dictionary
        global ncdim_size

        if dimsize is not None:
            if base in ncvar_names and dimsize == ncdim_size[base]:
                return base, counter

        if base in ncvar_names:
            ncvar = base + '_'+repr(counter)
            while ncvar in ncvar_names:
                counter += 1
                ncvar = base + '_'+repr(counter)
        else:
            ncvar = base

        ncvar_names.append(ncvar)            
        return ncvar, counter
    #--- End: def _check_name
    
    def _write_space(s, save=False):
        
        # Global netCDF4.Dataset
        global netcdf
        # Global sets
        global global_attributes
        # Global dictionaries
        global ncdim_size, dim_to_ncdim, nc, seen
        # Global counters
        global dataN, dimN, auxN, cmN, gmN

        if save:
            saved_dim_to_ncdim = copy.deepcopy(dim_to_ncdim)

        dim_to_ncdim = {}

        coordinates  = []

        for key in sorted(s.grid.dimension_sizes):            
            
            try:
                s.grid[key]
            except KeyError:
                # There is no dimension coordinate for this dimension,
                # so just create a netCDF dimension.
                try:
                    ncdim = s.grid.nc_dimensions[key]
                except (AttributeError, KeyError):
                    ncdim = 'dim'

                ncdim, dimN = _check_name(ncdim, dimN)         

                # Create a new dimension, if it has size > 1
                _create_dimension(ncdim, s, key)

            else:              
                # A dimension coordinate exists for this dimension, so
                # create a netCDF dimension and a netCDF coordinate
                # netCDF variable. 
                ncdim = _create_dimension_coordinate(s, key)

                if ncdim_size[ncdim] == 1:
                    coordinates.append(ncdim)

            #--- End: try...except...else...

        #--- End: for key

                
        # ----------------------------------------------------------------------
        # Create auxiliary coordinate variables, except those which
        # might be completely specified elsewhere by a transformation.
        # ----------------------------------------------------------------------
        # Initialize the list of 'coordinates' attribute variable
        # values (each of the form 'name')
        virtual_auxiliaries = []
        for key in sorted(s.grid.get_keys('aux')):

            # Find out if this auxiliary coordinate is a virtual
            # coordinate
            try:
                label = s.grid[key].transform
            except AttributeError:
                # No transform attribute => not a virtual coordinate
                pass
            else:
                # Got a transform attribute => could be a virtual coordinate
                try:
                    s.grid.transform[label]
                except (AttributeError, KeyError):
                    # Labelled transformation does not exist => not a
                    # virtual coordinate
                    pass
                else:
                    try:
                        s.grid.transform[label]['grid_mapping_name']
                    except KeyError:
                        try:
                            if s.grid[key].transform != s.grid[key].standard_name:
                                # Coordinate has a formula_terms transform
                                # whose name does not match its
                                # standard_name
                                virtual_auxiliaries.append(key)
                                continue 
                        except AttributeError: 
                            pass
          
#            try:
#               if s.grid[key].transform != s.grid[key].standard_name:
#                   virtual_auxiliaries.append(key)
#                   continue         
#           except AttributeError:
#               pass
            
            # Still here? Then this auxiliary coordinate might need
            # creating
            coordinates = _create_auxiliary_coordinate(s, key, coordinates)
        #--- End: for key 

        # ----------------------------------------------------------------------
        # Create cell measures variables
        # ----------------------------------------------------------------------
        # Initialize the list of 'cell_measures' attribute values
        # (each of the form 'measure: name')
        cell_measures = []
        for key in sorted(s.grid.get_keys('cm')):
            
            ncdimensions = _grid_ncdimensions(s, key)
            if not _seen(s.grid[key], ncdimensions):
                ncvar, cmN = _grid_ncvar(s, key, 'cm', cmN)        

                # Create a new cell_measures variable
                _createVariable(ncvar, ncdimensions, s.grid[key],
                                data=data)
                
            cell_measures.append(s.grid[cm].standard_name+': ' + \
                                     seen[id(s.grid[key])]['ncvar'])
        #--- End: for key 

        # ----------------------------------------------------------------------
        # Create grid mapping variables
        # ----------------------------------------------------------------------
        # Initialize the list of 'grid_mapping' attribute variable
        # values (each of the form 'name')
        grid_mapping = []
        for key in s.grid:
            try:
                label = s.grid[key].transform
                s.grid.transform[label]['grid_mapping_name']
            except (AttributeError, KeyError):
                # Continue because this coordinate doesn't have a
                # grid_mapping
                continue
            
            # Still here? Then this coordinate has a grid_mapping
            # transform which may need creating.
            if not _seen(s.grid.transform[label]):
                try:
                    ncvar = s.grid.transform[label].ncvar
                except AttributeError:
                    ncvar = s.grid.transform[label]['grid_mapping_name']
                ncvar, gmN = _check_name(ncvar, gmN) 

                # Create a new grid_mapping variable
                _create_grid_mapping(ncvar, s.grid.transform[label])

                ncvar = seen[id(s.grid.transform[label])]['ncvar']
                grid_mapping.append(ncvar)
        #--- End: for key

        # -------------------------------------------------------
        # Create formula_terms attributes
        # -------------------------------------------------------
        for key in s.grid:

            try:
                label = s.grid[key].transform
            except AttributeError:
                # Continue because this coordinate doesn't have a
                # transform
                continue

            try:
                s.grid.transform[label]['grid_mapping_name']
                # Continue because this coordinate's transform is not
                # a formula_terms
                continue
            except KeyError:
                pass

            # Still here? Then this coordinate has a transform which
            # needs to be turned into a formula_terms attribute.

            # Create the formula_terms attribute string 
            formula_terms = []
            for term, var in s.grid.transform[label].iteritems():              
                if var is None:
                    # Do not create broken formula_terms
                    formula_terms = None
                    break
                
                if isinstance(var, Variable):                    
                    try:
                        seen[id(var)]
                    except KeyError:
                        # Value is a variable which hasn't been
                        # written yet (set save=True so we don't
                        # overwrite the dim_to_ncdim dictionary).
                        _write_space(var, save=True)
                    
                    formula_terms.append(term+': '+seen[id(var)]['ncvar'])
                else:
                    try:
                        formula_terms.append(term+': '+seen[id(s.grid[var])]['ncvar'])
                    except :
                        # Do not create broken formula_terms
                        formula_terms = None
                        break
            #--- End: for term, var 

            # Add the formula_terms attribute to the output variable
            if formula_terms:
                ncvar = seen[id(s.grid[key])]['ncvar']
                nc[ncvar].formula_terms = ' '.join(formula_terms)
        #--- End: for key 
                
        # ----------------------------------------------------------------------
        # Of the remaining auxiliary coordinates, only create those
        # which have data and are not fully specified by another
        # coordinate's formula_terms attribute. (Note that this has to
        # be done after all CF formula_terms attributes have been
        # created.)
        # ----------------------------------------------------------------------
        for key in virtual_auxiliaries:
            
            try:
                s.grid[key]._data
            except AttributeError:
                # Coordinate has no data, so don't write it disk
                # ******* todo: BUT WE SHOULD CHECK ON BOUNDS, TOO
                # ******
                continue

            # Try to find a coordinate whose standard_name matches
            # this auxiliary coordinate's transform name
            coord = s.grid.coord(s.grid[key].transform, exact=True)
            try:
                ncvar = seen[id(coord)]['ncvar']
                nc[ncvar].formula_terms
                continue
            except (AttributeError, KeyError):
                pass
            
            # Still here? Then this auxiliary coordinate is not
            # otherwise specified by a formula_terms attribute and has
            # some data, so needs to be created.
            coordinates = _create_auxiliary_coordinate(s, key, coordinates)
        #--- End: for key

        # ----------------------------------------------------------------------
        # Create the data variable
        # ----------------------------------------------------------------------
        try:
            ncvar = s.ncvar
        except AttributeError:
            ncvar = s.name(default='data')
            
        ncvar, dataN = _check_name(ncvar, dataN)   
        
        ncdimensions = ()
        for dim in s.grid.dimensions['data']:
            ncdimensions += (dim_to_ncdim[dim],)

        # Create a new data variable
        _createVariable(ncvar, ncdimensions, s,
                        omit=set(('cell_methods',)) | global_attributes,
                        data=data)

        # test ok to put list in
        try:
            nc[ncvar].flag_meanings = ' '.join(nc[ncvar].flag_meanings)
        except AttributeError:
            pass
        
        if cell_measures:
            nc[ncvar].cell_measures = ' '.join(cell_measures)           
        if coordinates:
            nc[ncvar].coordinates   = ' '.join(coordinates)
        if grid_mapping:
            nc[ncvar].grid_mapping  = ' '.join(grid_mapping)

        # CELL_METHODS
        try:
            cell_methods = copy.deepcopy(s.cell_methods)
        except AttributeError:
            pass
        else:
            for i in xrange(len(cell_methods)):
                for j in xrange(len(cell_methods[i]['dim'])):
                    dim = cell_methods[i]['dim'][j]
                    if dim is not None and not cell_methods[i]['no_coords'][j]:
                        cell_methods[i]['name'][j] = seen[id(s.grid[dim])]['ncvar']
            nc[ncvar].cell_methods = ' '.join(cell_methods.strings())
            
        # ----------------------------------------------------------
        # If we are saving the original dim_to_ncdim, copy back the
        # original
        # ----------------------------------------------------------
        if save:
            dim_to_ncdim = copy.deepcopy(saved_dim_to_ncdim)
    #--- End: def _write_space                
            
    def _create_global_attributes(spaces_in):
        '''Find the file's global attributes and write them to the
        netCDF4.Dataset. These are the global or non-standard CF space
        attributes which have identical values across all input
        spaces. Returns None.'''

        # Global netCDF4.Dataset
        global netcdf
        # Global sets
        global global_attributes

        data_attributes = ['add_offset',
                           'ancillary_variables',
                           'cell_measures',
                           'cell_methods',
                           'coordinates',
                           '_FillValue',
                           'flag_masks',
                           'flag_meanings',
                           'flag_values',
                           'rid_mapping',
                           'long_name',
                           'missing_value',
                           'scale_factor',
                           'standard_error_multiplier',
                           'standard_name',
                           'units',
                           'valid_max',
                           'valid_min',
                           'valid_range',
                           ]

        global_attributes = set(['comment',
                                 'Conventions',
                                 'history',
                                 'institution',
                                 'references',
                                 'source',
                                 'title',
                                 ])
        
        # Put all non-CF compliant attributes (i.e. attributes from
        # appendix A which do not have the 'Use' of 'D') into the
        # global_attributes list
        for s in spaces_in:
            for prop in s.ncatts() - (set(('cell_methods',)) | global_attributes):
                if prop not in data_attributes:
                    global_attributes.add(prop)

        # Remove attributes from the global_attributes list which have
        # different values in different data variables
        s0 = spaces_in[0]
        for attr in list(global_attributes):
            try:
                getattr(s0, attr)
            except AttributeError:
                global_attributes.remove(attr)
                continue

            for s in spaces_in[1:]:
                try:                    
                    if getattr(s, attr) != getattr(s0, attr):
                        global_attributes.remove(attr)
                        break
                except AttributeError:
                    global_attributes.remove(attr)
                    break

        try:
            global_attributes.remove('Conventions')
        except KeyError:
            pass
        netcdf.setncattr('Conventions', 'CF-1.5')

        for attr in global_attributes:
            netcdf.setncattr(attr, getattr(s0, attr))

        # Put Conventions back into the global attributes set so that
        # we know to exclude it from data variables' attributes
        global_attributes.add('Conventions')

    #--- End: def _create_global_attributes

    # ------------------------------------------------------------
    # Reset global variables. See notes in this module for the
    # definition of these global variables
    # ------------------------------------------------------------
    nc                = {}
    dim_to_ncdim      = {}
    ncdim_size        = {}
    seen              = {} 
    ncvar_names       = []
    global_attributes = set(())
    dimN              = 1
    auxN              = 1
    cmN               = 1
    dataN             = 1
    gmN               = 1
    bndN              = 1
    bnddimN           = 1
    strlenN           = 1

    # ---------------------------------------------------------------
    # Open the output netCDF file. (NETCDF3_CLASSIC can be *really*
    # slow)
    # ---------------------------------------------------------------
    netcdf = netCDF4.Dataset(file, 'w', format=format)

    # ---------------------------------------------------------------
    # Set the fill mode for a Dataset open for writing to off. This
    # will prevent the data from being pre-filled with fill values,
    # which may result in some performance improvements.
    # ---------------------------------------------------------------
    netcdf.set_fill_off()

    # ---------------------------------------------------------------
    # Convert a single space to a list of spaces
    # ---------------------------------------------------------------
    if isinstance(spaces, Space):
        spaces_in = SpaceList((spaces,))
    else:
        spaces_in = spaces
    spaces_in = spaces_in.extract(exact=exact, **kwargs)

    if not spaces_in:
        raise RuntimeError('ERROR: no spaces found')

    # ---------------------------------------------------------------
    # Write global attributes to the file first. This is important as
    # doing it later slows things down enormously. This function
    # creates the global global_attributes set, which is used in def
    # _write_space.
    # ---------------------------------------------------------------
    _create_global_attributes(spaces_in)

    # ---------------------------------------------------------------
    # ---------------------------------------------------------------
    for space in spaces_in:
        _write_space(space)
        if verbose:
            print repr(space)
        
    # ---------------------------------------------------------------
    # Close the netCDF Dataset, writing all buffered data to disk
    # ---------------------------------------------------------------
    netcdf.close()

    if verbose:
        n = len(spaces_in)
        print 'Wrote',repr(n),'space'+('s' if n>1 else '')+' to',file
#--- End: def write
